define(['jquery', 'video'],
function($, Video) {
	var CPU = function() {
		var i;
		this.V = [];
		for (i = 0; i < CPU._N_REGISTERS; i++)
			this.V[i] = 0;
		this.I = 0;
		this.PC = Chip8._START_ADDRESS;
		this.SP = 0;
		this.DT = 0;
		this.ST = 0;
	}

	CPU._N_REGISTERS = 16;

	var OpCode = function(opcode) {
		this.main = (opcode >> 12) & 0x0F;
		this.nnn = opcode & 0x0FFF;
		this.kk = opcode & 0xFF;
		this.x = (opcode >> 8) & 0x0F;
		this.y = (opcode >> 4) & 0x0F;
		this.n = opcode & 0x0F;
	}

	var Chip8 = function(frequency) {
		var i;

		// Initialize CPU
		this._cpu = new CPU();

		// Initialize memory
		this._memory = [
			0xF0, 0x90, 0x90, 0x90, 0xF0,
			0x20, 0x60, 0x20, 0x20, 0x70,
			0xF0, 0x10, 0xF0, 0x80, 0xF0,
			0xF0, 0x10, 0xF0, 0x10, 0xF0,
			0x90, 0x90, 0xF0, 0x10, 0x10,
			0xF0, 0x80, 0xF0, 0x10, 0xF0,
			0xF0, 0x80, 0xF0, 0x90, 0xF0,
			0xF0, 0x10, 0x20, 0x40, 0x40,
			0xF0, 0x90, 0xF0, 0x90, 0xF0,
			0xF0, 0x90, 0xF0, 0x10, 0xF0,
			0xF0, 0x90, 0xF0, 0x90, 0x90,
			0xE0, 0x90, 0xE0, 0x90, 0xE0,
			0xF0, 0x80, 0x80, 0x80, 0xF0,
			0xE0, 0x90, 0x90, 0x90, 0xE0,
			0xF0, 0x80, 0xF0, 0x80, 0xF0,
			0xF0, 0x80, 0xF0, 0x80, 0x80
		];

		// Initialize stack
		this._stack = [];

		// Initialize screen
		Video.initScreen(Chip8._SCREEN_WIDTH, Chip8._SCREEN_HEIGHT);
		this._screen = [];
		for (i = 0; i < Chip8._SCREEN_WIDTH; i++)
			this._screen[i] = [];

		// Set machine frequency
		this._frequency = frequency;

		// Set instance for later use
		Chip8._instance = this;
	}

	Chip8._MEMORY_SIZE = 3584;
	Chip8._START_ADDRESS = 0x0200;
	Chip8._SCREEN_WIDTH = 64;
	Chip8._SCREEN_HEIGHT = 32;
	Chip8._CHAR_SIZE = 5;
	Chip8._N_PIXELS_PER_BYTE = 8;

	Chip8.prototype = {
		_CLS: function() {
			var i, j;
			for (i = 0; i < Chip8._SCREEN_WIDTH; i++)
				for (j = 0; j < Chip8._SCREEN_HEIGHT; j++)
					this._screen[i][j] = 0;
		},

		_RET: function() {
			this._cpu.PC = this._stack[--this._cpu.SP];
		},

		_SYS_addr: function() {
		},

		_JP_addr: function() {
			this._cpu.PC = this._cpu.opcode.nnn;
		},

		_CALL_addr: function() {
			this._stack[this._cpu.SP++] = this._cpu.PC;
			this._cpu.PC = this._cpu.opcode.nnn;
		},

		_SE_Vx_byte: function() {
			if (this._cpu.V[this._cpu.opcode.x] == this._cpu.opcode.kk)
				this._cpu.PC += 2;
		},

		_SNE_Vx_byte: function() {
			if (this._cpu.V[this._cpu.opcode.x] != this._cpu.opcode.kk)
				this._cpu.PC += 2;
		},

		_SE_Vx_Vy: function() {
			var x = this._cpu.opcode.x;
			var y = this._cpu.opcode.y;
			if (this._cpu.V[x] == this._cpu.V[y])
				this._cpu.PC += 2;
		},

		_LD_Vx_byte: function() {
			this._cpu.V[this._cpu.opcode.x] = this._cpu.opcode.kk;
		},

		_ADD_Vx_byte: function() {
			this._cpu.V[this._cpu.opcode.x] += this._cpu.opcode.kk;
			this._cpu.V[this._cpu.opcode.x] &= 0xFF;
		},

		_LD_Vx_Vy: function() {
			this._cpu.V[this._cpu.opcode.x] = this._cpu.V[this._cpu.opcode.y];
		},

		_OR_Vx_Vy: function() {
			this._cpu.V[this._cpu.opcode.x] |= this._cpu.V[this._cpu.opcode.y];
		},

		_AND_Vx_Vy: function() {
			this._cpu.V[this._cpu.opcode.x] &= this._cpu.V[this._cpu.opcode.y];
		},

		_XOR_Vx_Vy: function() {
			this._cpu.V[this._cpu.opcode.x] ^= this._cpu.V[this._cpu.opcode.y];
		},

		_ADD_Vx_Vy: function() {
			var x = this._cpu.opcode.x;
			var y = this._cpu.opcode.y;
			var result = this._cpu.V[x] + this._cpu.V[y];
			this._cpu.V[x] = result & 0xFF;
			this._cpu.V[0x0F] = (result > 0xFF);
		},

		_SUB_Vx_Vy: function() {
			var x = this._cpu.opcode.x;
			var y = this._cpu.opcode.y;
			this._cpu.V[0x0F] = (this._cpu.V[x] > this._cpu.V[y]);
			this._cpu.V[x] -= this._cpu.V[y];
			this._cpu.V[x] &= 0xFF;
		},

		_SHR_Vx: function() {
			var x = this._cpu.opcode.x;
			this._cpu.V[0x0F] = ((this._cpu.V[x] & 0x01) != 0);
			this._cpu.V[x] >>= 1;
		},

		_SUBN_Vx_Vy: function() {
			var x = this._cpu.opcode.x;
			var y = this._cpu.opcode.y;
			this._cpu.V[0x0F] = (this._cpu.V[y] > this._cpu.V[x]);
			this._cpu.V[x] = this._cpu.V[y] - this._cpu.V[x];
			this._cpu.V[x] &= 0xFF;
		},

		_SHL_Vx: function() {
			var x = this._cpu.opcode.x;
			this._cpu.V[0x0F] = ((this._cpu.V[x] & 0x80) != 0);
			this._cpu.V[x] <<= 1;
			this._cpu.V[x] &= 0xFF;
		},

		_SNE_Vx_Vy: function() {
			var x = this._cpu.opcode.x;
			var y = this._cpu.opcode.y;
			if (this._cpu.V[x] != this._cpu.V[y])
				this._cpu.PC += 2;
		},

		_LD_I_addr: function() {
			this._cpu.I = this._cpu.opcode.nnn;
		},

		_JP_V0_addr: function() {
			this._cpu.PC = this._cpu.opcode.nnn + this._cpu.V[0];
		},

		_RND_Vx_byte: function() {
			var r = Math.floor((Math.random() * 256));
			this._cpu.V[this._cpu.opcode.x] = r & this._cpu.opcode.kk;
		},

		_DRW_Vx_Vy_nibble: function() {
			var i, j, x, y, b, src, VF;
			VF = 0;
			for (i = 0; i < this._cpu.opcode.n; i++) {
				b = this._memory[this._cpu.I + i];
				y = (this._cpu.V[this._cpu.opcode.y] + i) %
					Chip8._SCREEN_HEIGHT;
				for (j = 0; j < Chip8._N_PIXELS_PER_BYTE; j++) {
					x = (this._cpu.V[this._cpu.opcode.x] + j) %
						Chip8._SCREEN_WIDTH;
					src = b >> (Chip8._N_PIXELS_PER_BYTE - j - 1) & 0x01;
					this._screen[x][y] ^= src;
					if (src && (this._screen[x][y] == 0))
						VF = 1;
				}
			}
			this._cpu.V[0x0F] = VF;
		},

		_SKP_Vx: function() {
		},

		_SKNP_Vx: function() {
			this._cpu.PC += 2;
		},

		_LD_Vx_DT: function() {
			this._cpu.V[this._cpu.opcode.x] = this._cpu.DT;
		},

		_LD_Vx_K: function() {
			this._cpu.PC -= 2;
		},

		_LD_DT_Vx: function() {
			this._cpu.DT = this._cpu.V[this._cpu.opcode.x];
		},

		_LD_ST_Vx: function() {
			this._cpu.ST = this._cpu.V[this._cpu.opcode.x];
		},

		_ADD_I_Vx: function() {
			this._cpu.I += this._cpu.V[this._cpu.opcode.x];
		},

		_LD_F_Vx: function() {
			this._cpu.I = (this._cpu.V[this._cpu.opcode.x] & 0x0F) *
				Chip8._CHAR_SIZE;
		},

		_LD_B_Vx: function() {
			var x = this._cpu.opcode.x;
			this._memory[this._cpu.I] = this._cpu.V[x] / 100;
			this._memory[this._cpu.I + 1] = (this._cpu.V[x] / 10) % 10;
			this._memory[this._cpu.I + 2] = this._cpu.V[x] % 10;
		},

		_LD_cI_Vx: function() {
			var i;
			for (i = 0; i <= this._cpu.opcode.x; i++)
				this._memory[this._cpu.I + i] = this._cpu.V[i];
		},

		_LD_Vx_cI: function() {
			var i;
			for (i = 0; i <= this._cpu.opcode.x; i++)
				this._cpu.V[i] = this._memory[this._cpu.I + i];
		},

		_fetch: function() {
			var o1, o2;
			o1 = this._memory[this._cpu.PC++];
			o2 = this._memory[this._cpu.PC++];
			this._cpu.opcode = new OpCode((o1 << 8) | o2);
		},

		_opcode_0: function() {
			switch (this._cpu.opcode.kk) {
			case 0xE0: this._CLS(); break;
			case 0xEE: this._RET(); break;
			default: this._SYS_addr(); break;
			}
		},

		_opcode_8: function() {
			switch (this._cpu.opcode.n) {
			case 0x00: this._LD_Vx_Vy(); break;
			case 0x01: this._OR_Vx_Vy(); break;
			case 0x02: this._AND_Vx_Vy(); break;
			case 0x03: this._XOR_Vx_Vy(); break;
			case 0x04: this._ADD_Vx_Vy(); break;
			case 0x05: this._SUB_Vx_Vy(); break;
			case 0x06: this._SHR_Vx(); break;
			case 0x07: this._SUBN_Vx_Vy(); break;
			case 0x0E: this._SHL_Vx(); break;
			}
		},

		_opcode_E: function() {
			switch (this._cpu.opcode.kk) {
			case 0x9E: this._SKP_Vx(); break;
			case 0xA1: this._SKNP_Vx(); break;
			}
		},

		_opcode_F: function() {
			switch (this._cpu.opcode.kk) {
			case 0x07: this._LD_Vx_DT(); break;
			case 0x0A: this._LD_Vx_K(); break;
			case 0x15: this._LD_DT_Vx(); break;
			case 0x18: this._LD_ST_Vx(); break;
			case 0x1E: this._ADD_I_Vx(); break;
			case 0x29: this._LD_F_Vx(); break;
			case 0x33: this._LD_B_Vx(); break;
			case 0x55: this._LD_cI_Vx(); break;
			case 0x65: this._LD_Vx_cI(); break;
			}
		},

		load: function(rom) {
			var result = false;
			var chip8 = this;

			// Load ROM synchronously
			var xhr = new XMLHttpRequest();
			xhr.open('GET', 'roms/' + rom, false);
			xhr.responseType = 'arraybuffer';
			xhr.onload = function() {
				var i, contents;

				// Validate ROM has been found
				if (this.response == null)
					return;

				// Copy ROM contents into machine memory
				i = 0;
				contents = new Uint8Array(this.response);
				while ((i < contents.length) && (i < Chip8._MEMORY_SIZE)) {
					chip8._memory[Chip8._START_ADDRESS + i] = contents[i];
					i++;
				}
				result = true;
			};
			xhr.send();

			// Return whether ROM is successfully loaded or not
			return result;
		},

		run: function() {
			var i, x, y, nCycles;

			// Get Chip8 instance
			chip8 = Chip8._instance;

			// Calculate delay and derive number of cycles to execute
			nCycles = Math.floor((chip8._frequency / 60) + 1);

			// Fetch opcodes and execute instructions
			for (i = 0; i < nCycles; i++) {
				chip8._fetch();
				switch (chip8._cpu.opcode.main) {
				case 0x00: chip8._opcode_0(); break;
				case 0x01: chip8._JP_addr(); break;
				case 0x02: chip8._CALL_addr(); break;
				case 0x03: chip8._SE_Vx_byte(); break;
				case 0x04: chip8._SNE_Vx_byte(); break;
				case 0x05: chip8._SE_Vx_Vy(); break;
				case 0x06: chip8._LD_Vx_byte(); break;
				case 0x07: chip8._ADD_Vx_byte(); break;
				case 0x08: chip8._opcode_8(); break;
				case 0x09: chip8._SNE_Vx_Vy(); break;
				case 0x0A: chip8._LD_I_addr(); break;
				case 0x0B: chip8._JP_V0_addr(); break;
				case 0x0C: chip8._RND_Vx_byte(); break;
				case 0x0D: chip8._DRW_Vx_Vy_nibble(); break;
				case 0x0E: chip8._opcode_E(); break;
				case 0x0F: chip8._opcode_F(); break;
				}
			}

			// Update timers
			if (chip8._cpu.DT > 0)
				chip8._cpu.DT--;
			if (chip8._cpu.ST > 0)
				chip8._cpu.ST--;

			// Draw screen
			for (x = 0; x < Chip8._SCREEN_WIDTH; x++)
				for (y = 0; y < Chip8._SCREEN_HEIGHT; y++)
					Video.setPixel(x, y, chip8._screen[x][y] ? 0xFFFFFF : 0);
			Video.update();

			// Step machine again
			window.setTimeout(chip8.run, 1000 / 60);
		}
	};

	return Chip8;
});